/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University. All rights reserved.
 * 
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license; 2) All redistributions in binary form
 * must reproduce the above copyright notice, this list of conditions and the
 * disclaimer listed in this license in the documentation and/or other materials
 * provided with the distribution; 3) Any documentation included with all
 * redistributions must include the following acknowledgement:
 * 
 * "This product includes software developed by the Community Grids Lab. For
 * further information contact the Community Grids Lab at
 * http://communitygrids.iu.edu/."
 * 
 * Alternatively, this acknowledgement may appear in the software itself, and
 * wherever such third-party acknowledgments normally appear.
 * 
 * 4) The name Indiana University or Community Grids Lab or NaradaBrokering,
 * shall not be used to endorse or promote products derived from this software
 * without prior written permission from Indiana University. For written
 * permission, please contact the Advanced Research and Technology Institute
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202. 5) Products
 * derived from this software may not be called NaradaBrokering, nor may Indiana
 * University or Community Grids Lab or NaradaBrokering appear in their name,
 * without prior written permission of ARTI.
 * 
 * 
 * Indiana University provides no reassurances that the source code provided
 * does not infringe the patent or any other intellectual property rights of any
 * other entity. Indiana University disclaims any liability to any recipient for
 * claims brought by any other entity based on infringement of intellectual
 * property rights or otherwise.
 * 
 * LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO
 * WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 * NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS.
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS",
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION
 * GENERATED USING SOFTWARE.
 */
package cgl.narada.protocol;

import java.util.Vector;

import cgl.narada.event.NBEvent;
import cgl.narada.matching.Profile;
import cgl.narada.matching.ProfileMatchingTrees;
import cgl.narada.matching.ProfileRequest;
import cgl.narada.service.security.kmc.SignedSecurityToken;

/**
 * This determines if a profile change needs to be propagated to a higher level
 * node, and is responsible for doing so. In addition, a node could be gateway
 * at multiple levels. Depending on the level for which profile changes have
 * taken place, the propagation needs to be done accordingly.
 * 
 * @author Shrideep Pallickara $Revision$ $Date$
 */

public class ProfilePropagationProtocol implements ProtocolDebugFlags {

    /* Gateway Information */
    private GatewayInfo gatewayInfo;

    /**
     * Gateways Indicator - This provides indication regarding wether this node
     * is a gateway at certain levels. By default every node is a level-0
     * gateway
     */
    private boolean[] gatewayIndicator;

    /**
     * Gateways at level : This provides us with a list of level-l gateways
     * within the level-l unit
     */
    private Vector gatewaysAtLevel;

    private int systemLevel;

    private Destinations thisDestination;

    private ProfileMatchingTrees profileMatchingTrees;

    /** Protocol Handler */
    private ProtocolHandler protocolHandler;

    private String moduleName = "ProfilePropagationProtocol: ";

    public ProfilePropagationProtocol(GatewayInfo gatewayInfo,
        ProtocolHandler protocolHandler) {
        this.gatewayInfo = gatewayInfo;
        systemLevel = gatewayInfo.getSystemLevel();
        /**
         * There is a change from systemLevel+1 to systemLevel. This is to
         * account for the fact that we are interested only about level-l
         * gatekeepers within the level-l unit
         */
        gatewaysAtLevel = new Vector();
        for (int i = 0; i < systemLevel + 1; i++) {
            gatewaysAtLevel.addElement(new Vector());
        }

        thisDestination = protocolHandler.getNodeDestination();

        profileMatchingTrees = new ProfileMatchingTrees(systemLevel,
                                                        thisDestination, this);

        this.protocolHandler = protocolHandler;
        gatewayIndicator = new boolean[systemLevel + 1];
        for (int i = 0; i < systemLevel + 1; i++) {
            gatewayIndicator[i] = false;
            // ::
            profileMatchingTrees.initializeMatchingTreesAtLevel(i);
        }
        gatewayIndicator[0] = true;
    }

    public void setDestination(Destinations destination) {
        thisDestination = destination;
    }

    /**
     * This method indicates if the node in question is a gateway at the level
     * in the argument
     * 
     * @param level
     *            The level for which we are testing if the node is a gateway.
     */
    public boolean isGatewayAtLevel(int level) {
        return gatewayIndicator[level];
    }

    /** This designates this node as a gateway at level <i>level</i>. */
    public void setAsGatewayAtLevel(int level) {
        System.out.println(moduleName + "setting as gateway at level (" + level
            + ")");
        gatewayIndicator[level] = true;
        // ::
        /**
         * if (level != 0) {
         * profileMatchingTrees.initializeMatchingTreesAtLevel(level); }
         */
    }

    /**
     * This method is called only if the isGatewayAtLevel returns <i>true</i>.
     * This method computes the destinations associated with the matching of an
     * event at a particular level
     * 
     * @param nbEvent
     *            The event that needs to be matched.
     * @param level
     *            The destinations at level (<i>level-1</i>) that we are
     *            interested in.
     * @return The destinations that are associated with the event.
     */
    public Object computeEventDestinationsAtLevel(NBEvent nbEvent, int level) {
        // ::
        /**
         * if (!gatewayIndicator[level + 1]) { System.out.println(moduleName +
         * "JmsPP:computeDestinationsAtLevel-> Wrong invocation" + " of the
         * method for computing destinations at level" + "(" + level + ")");
         * return new Integer(0); }
         */

        return profileMatchingTrees.computeEventDestinations(nbEvent, level);
    }

    /**
     * Handle profile propagation requests. In some cases based on the toReach
     * destinations that are received we need to provide the destination address
     * more accurately.
     */
    public void processProfilePropagationRequest(byte[] requestBytes,
                                                 String sender) {
        ProfileRequest profileRequest = new ProfileRequest(requestBytes);
        Profile profile = profileRequest.getProfile();
        boolean addProfile = profileRequest.isAddProfile();
        boolean isFromClient = profileRequest.isFromClient();

        // HG:
        // If the Profile represents a secure topic, then verify security
        // token
        Object contentSynopsis = profile.getSubscription();
        int sysopsisType = profile.getProfileType();

        boolean secureTopic = protocolHandler
            .sysnopsisRepresentsSecureTopic(contentSynopsis, sysopsisType);

        boolean accessAllowed = false;

        String message = "";
        if (secureTopic) {
            // OK, this is a secure topic...
            // Check if a security token was sent
            if (profileRequest.hasSignedSecurityToken()) {
                byte[] sstBytes = profileRequest.getSignedSecurityToken();
                SignedSecurityToken sst = SignedSecurityToken
                    .createObjectFromBytes(sstBytes);

                // Ok verify the access and the token
                if (protocolHandler.verifySignedSecurityToken(sst)) {
                    // Finally check if token bearer has subscribe rights if its
                    // a registered secure topic

                    if (!sst.getSecurityToken().getRights().hasSubscribeRight()) {
                        message = "Entity has no Subscribe rights !";
                        accessAllowed = false;
                    } else
                        accessAllowed = true;
                } else {
                    accessAllowed = false;
                    message = "Could not verify SignedSecurityToken";
                }
            } else {
                // Ahhaa... No security token... SO
                message = "Security Token required for access to secure topic !";
            }
        } else
            accessAllowed = true; // Temporarily allow access, perhaps we'll
        // know abt. the topic nature later

        if (!accessAllowed) {

            if (isFromClient) {
                // Send error response to sender
                byte[] msgBytes = message.getBytes();
                byte[] messageWithHeader = new byte[msgBytes.length + 1];
                messageWithHeader[0] = Protocol.CLIENT_SETUP_FAILURE;
                System.arraycopy(msgBytes, 0, messageWithHeader, 1,
                                 msgBytes.length);
                protocolHandler.routeMessageToClient(sender, profile
                    .getDestination(), messageWithHeader);
            } else {
                // This is a Profile propagation from broker...
                // so notify broker to update its secure topic listing...

                // NOTE: Broker auto updates its listing when he recieves a
                // secure topic
            }

            return;
        }
        // ELSE proceed w/ proceesing

        // In either case, if there is a secure token, We store the signed
        // security token in a table indexed by the destination ID
        if (profileRequest.hasSignedSecurityToken()) {
            System.out.println("Storing Securitytoken for Profile: "
                + profile.getProfileId());
            protocolHandler.registerProfileSecurityToken(new Integer(profile
                .getDestination()), profileRequest.getSignedSecurityToken());
        }

        // ----

        if (isFromClient) {
            if (ProfilePropagationProtocol_Debug) {
                System.out.println(moduleName + "Processing Client Request");
            }
            profileMatchingTrees.manageSubscriptionProfile(profile, addProfile,
                                                           -1);
            return;
        }

        int level = profileRequest.getLevel();
        profileMatchingTrees.manageSubscriptionProfile(profile, addProfile,
                                                       level);

        Destinations toReach = new Destinations(profileRequest
            .getDestinationsToReach());
        Destinations traversedSoFar = new Destinations(profileRequest
            .getDestinationsTraversedSoFar());
        traversedSoFar.markAsTraversedAt(thisDestination);

        computeDestinationsToReach(toReach);

        disseminateProfileChange(profile, level, addProfile, toReach,
                                 traversedSoFar);

    }

    /**
     * Compute the destinations to reach, based on the destinations that have
     * been reached so far.
     * 
     * @param toReach
     */
    private void computeDestinationsToReach(Destinations toReach) {

        int level = 0;
        int destAtLevel = 0;

        while (destAtLevel == 0) {
            destAtLevel = toReach.getDestinationsAtLevel(level);

            if (destAtLevel == 0) {
                level++;
            }
        }

        if (ProfilePropagationProtocol_Debug) {
            System.out.println(moduleName + "Profiles received over a level ("
                + level + ") link");
        }

        if (level != 0) {
            if (ProfilePropagationProtocol_Debug) {
                System.out.println(moduleName + "computeDest() To Reach:\n"
                    + toReach);
            }
        }

        Destinations knownDestinations = gatewayInfo
            .getListOfDestinationsKnownToNode();

        for (int i = level - 1; i >= 0; i--) {
            int destsAtLevelI = knownDestinations.getDestinationsAtLevel(i);
            toReach.setDestinationsAtLevel(i, destsAtLevelI);
        }

        if (level != 0) {
            if (ProfilePropagationProtocol_Debug) {
                System.out.println(moduleName
                    + "Newly computed destinations = " + toReach);
            }
        }
    }

    /**
     * This method is responsible for propagating profile changes to trees at
     * the next highest level.
     * 
     * @param profile
     *            This is the subscription profile that needs to be propagated.
     * @param level
     *            The level for which this change is being propagated
     * @param addProfile
     *            A boolean variable indicating wether this profile is being
     *            added to the system or is being removed.
     */
    public void propagateProfileChange(Profile profile, int level,
                                       boolean addProfile) {

        if (ProfilePropagationProtocol_Debug) {
            System.out.println(moduleName + ".propagateProfile() level ="
                + level + " >> " + profile);
        }

        profileMatchingTrees.manageSubscriptionProfile(profile, addProfile,
                                                       level);

        Destinations knownDestinations = gatewayInfo
            .getListOfDestinationsKnownToNode();

        Destinations destinationToReach = new Destinations(systemLevel);
        for (int i = 0; i <= level; i++) {
            int destsAtLevelI = knownDestinations.getDestinationsAtLevel(i);
            destinationToReach.setDestinationsAtLevel(i, destsAtLevelI);
        }
        if (ProfilePropagationProtocol_Debug) {
            System.out.println("Level (" + level
                + ") profile update to be sent to \n" + destinationToReach);
        }

        Destinations reachedSoFar = new Destinations(systemLevel);
        disseminateProfileChange(profile, level, addProfile,
                                 destinationToReach, reachedSoFar);

    }

    /**
     * Disseminate the profile change to the specified locations.
     * 
     * @param profile
     *            This is the subscription profile that needs to be propagated.
     * @param level
     *            The level for which this change is being propagated
     * @param addProfile
     *            A boolean variable indicating wether this profile is being
     *            added to the system or is being removed.
     * @param toReach
     *            Destinations that need to be reached
     * @param reachedSoFar
     *            Destinations that have been traversed so far.
     */
    private void disseminateProfileChange(Profile profile, int level,
                                          boolean addProfile,
                                          Destinations toReach,
                                          Destinations reachedSoFar) {

        reachedSoFar.markAsTraversedAt(thisDestination);
        if (ProfilePropagationProtocol_Debug) {
            System.out.println(moduleName + "\tLevel = (" + level
                + ")\n ToReach " + toReach + "ReachedSoFar" + reachedSoFar);
        }
        Gateway[] hopsToTake = gatewayInfo.hopsToReachDestination(toReach,
                                                                  reachedSoFar);

        if (hopsToTake != null) {
            for (int i = 0; i < hopsToTake.length; i++) {
                Gateway _gateway = hopsToTake[i];
                if (_gateway.getLevel() > level) {
                    if (ProfilePropagationProtocol_Debug) {
                        System.out.println(moduleName + "Ignoring gateway "
                            + _gateway);
                    }
                    continue;
                }
                reachedSoFar.updateDestinationList(_gateway);
                if (ProfilePropagationProtocol_Debug) {
                    System.out.println(moduleName + "Taking hop =>" + _gateway);
                }
            }
        }

        /** if connection.level < hop.level ... don not send over the hop */
        if (hopsToTake != null) {
            for (int i = 0; i < hopsToTake.length; i++) {
                if (hopsToTake[i].getLevel() > level) {
                    /**
                     * This hop can't be used since we are not supposed to route
                     * it over a higher level hop. This scenario will not take
                     * place, and should signal an error somewhere.
                     */
                    if (ProfilePropagationProtocol_Debug) {
                        System.out.println(moduleName
                            + " Hop returned has level "
                            + " greater than the level of change");
                    }
                    continue;

                }
                /**
                 * Depending on the kind of hop that you are gonna be sending
                 * the connection ... send the relevant connection information
                 * over the link
                 */
                byte[] byteStream = createProfilePropagationRequest(
                                                                    profile,
                                                                    level,
                                                                    addProfile,
                                                                    toReach,
                                                                    reachedSoFar,
                                                                    hopsToTake[i]
                                                                        .getLevel());
                protocolHandler.sendToNode(hopsToTake[i], byteStream);
                if (ProfilePropagationProtocol_Debug) {
                    System.out.println(moduleName + "change at level(" + level
                        + ") [[" + profile
                        + "]] ==>  Byte stream sent over hop " + hopsToTake[i]);
                }
            }
            return;
        }

    }

    private byte[] createProfilePropagationRequest(Profile profile, int level,
                                                   boolean addProfile,
                                                   Destinations toReach,
                                                   Destinations reachedSoFar,
                                                   int hopLevel) {

        ProfileRequest profileRequest = new ProfileRequest(profile, addProfile,
                                                           level);

        byte[] toReachBytes = toReach
            .prepareDestinationsToSendOverLevel(hopLevel);
        byte[] traversedSoFarBytes = reachedSoFar
            .prepareDestinationsToSendOverLevel(hopLevel);

        profileRequest.setDestinationsToReach(toReachBytes);
        profileRequest.setDestinationsTraversedSoFar(traversedSoFarBytes);

        return profileRequest.getBytes();
    }

    public void connectionToClientLost(Object clientIdentifier, String linkId) {
        System.out.println(moduleName + "Connection to [" + clientIdentifier
            + "] lost");
        profileMatchingTrees.connectionToClientLost(clientIdentifier, linkId);
    }

    /**
     * Marshall the profiles from a specified level onwards to a newly added
     * node.
     * 
     * @param level
     * @return
     */
    public byte[] marshallProfilesForSpecifiedLevelOnwards(int level) {
        return profileMatchingTrees
            .marshallProfilesForSpecifiedLevelOnwards(level);
    }

    /**
     * Process the profiles bytes that have been received.
     * 
     * @param marshalledBytes
     */
    public void processMarshalledProfiles(byte[] marshalledBytes) {
        profileMatchingTrees.processMarshalledProfiles(marshalledBytes);
    }

    public static void main(String[] args) {

    }

}
